import axios from 'axios'
import {AxiosResponse} from 'axios'
import { Crypt,Lang } from 'c3-util'
import {catchError,concatMap, first,from,map,Observable, of, switchMap, tap } from 'rxjs'
import {Optional} from 'c3-util'
import {IConsoleLogger} from '../jsjac'
import {JSJaCConsoleLogger}  from '../jsjac'
import {SoaResponseInfo} from './SoaResponseInfo'

const ACCESSTOKEN_SERVICE='GetAccessToken'
const DEFAULT_TIMEOUT=10000 //10s超时
const AXIOS_STATUS_TIMEOUT=110 //调用服务超时,TODO，服务超时的具体代码还不清楚

/**
 * 调用soaService的服务实现类,
 * 注意，必须设置完成 setStaffurls()后才允许调用 callStaffService服务
 */
export class SoaService {
    private spdnUrls:string[] //urls[0]:表示在用的地址,0后面的是备用的urls地址 
    private accessToken:string=null
    private accessTokenExpireTime:Date=null
    private log:IConsoleLogger
    private currSpdnUrl:string=null
    private staffUrls:string[]
    private currStaffUrl:string
    constructor(spdnUrls:string[],log:IConsoleLogger=new JSJaCConsoleLogger()){
        if( Lang.isEmpty(spdnUrls) && spdnUrls.length<1){
            log.error('the spdnUrls is empty or not exits please check spdnUrls params')
            throw new Error('the spdnUrls is empty or not exits please check spdnUrls params')
        }
        this.spdnUrls=spdnUrls
        this.log=log
        this.currSpdnUrl=spdnUrls[0]
    }

    /**
     * 设置staffUrls的urls数组
     * @param staffUrls 调用staffUrls的url地址
     */
    public setStaffUrls(staffUrls:Array<string>):void{
        this.staffUrls=staffUrls
        this.currStaffUrl=staffUrls[0]
    }

    public getCurrStaffUrl():string {
        return this.currStaffUrl
    }

    /**
     * 设置当前的spdn地址,如果spdn地址不在spdn列表，就加入并且设置为当前spdn地址，
     * 如果已经存在就设置当前spdn地址 
     * @param spdnUrl spdn地址
     */
    public changeCurrSpdnUrl(spdnUrl:string):boolean{
        if(Lang.isEmpty(spdnUrl) && !spdnUrl.endsWith('common/')){
            this.log.warn('set spdn url error,spdn url is invalid :'+spdnUrl)
            return false
        }
        this.log.debug('set spdn url to:'+spdnUrl)
        if(this.spdnUrls.find(url=>url===spdnUrl)){
            this.currSpdnUrl=spdnUrl
        } else {
            this.spdnUrls.splice(0,0,spdnUrl)
        }
        return true
    }
    /**
     * 验证accessTokn到期时间
     * @returns accessToken到期时间
     */
    public getAccessTokenExpire():Optional<Date> {
        return Optional.ofNullable(this.accessTokenExpireTime)
    }

    /**
     * 调用 callStaff类型的服务
     * @param companyId 公司id
     * @param staffId 员工id
     * @param serviceName 服务名
     * @param inputBody 输入参数对象
     * @returns 观察者
     */
    public callStaffService(companyId:string,staffId:string,
        serviceName:string,inputBody:object,recursionNum:number=0):Observable<SoaResponseInfo>{

        if(Lang.isEmpty(companyId)){
            const errResponse=new SoaResponseInfo(serviceName)
            errResponse.setResultCode(-20)
            errResponse.setResultMessage('service url is empty. not set staff spdnUrls')
            return of(errResponse)
        }
        if(Lang.isEmpty(serviceName)){
            let errResponse=new SoaResponseInfo('unknowService')
            errResponse.setResultCode(-19)
            errResponse.setResultMessage('serviceName is empty')
            return of(errResponse)
        } else if(Lang.isEmpty(inputBody)){
            let errResponse=new SoaResponseInfo(serviceName)
            errResponse.setResultCode(-19)
            errResponse.setResultMessage('inputParam is empty')
            return of(errResponse)
        }
        //获取accessToken的观察者
        let accessToken$=this.getAccessToken(companyId,staffId)
        //获取调用服务的观察者
        let staffService$=accessToken$.pipe(
            switchMap(accessT =>{
                return this.callService(companyId,staffId,serviceName,accessT,inputBody,
                    DEFAULT_TIMEOUT,this.currStaffUrl)
            })
        )
        //获取 soaResponse的结果的观察者
        let soaResponse$=staffService$.pipe(
            switchMap(axiosResp=>{
                //  调用accessToken过期或者无效处理
                if(axiosResp.status==200 && 
                    axiosResp.data.head.result==='4002' &&
                    recursionNum<2){
                    this.log.warn('accessToken is invalid. so retry getAccessToken')
                    //清空accessToken
                    this.accessToken=null
                    this.accessTokenExpireTime=null
                    //   这里保证不死循环
                    recursionNum=recursionNum+1
                    return this.callStaffService(companyId,staffId,serviceName,inputBody,recursionNum)

                }
                const optionSoaResp=this.convertAxiosResponseToSoaResponse(axiosResp,serviceName)
                if(optionSoaResp.isPresent()){
                  return of(optionSoaResp.get())
                }
               //最终可能是应用服务器异常，切换备用IP地址
               return this.switchBackupUrl(companyId,staffId,serviceName,inputBody,axiosResp)

            })
        )
        return soaResponse$
    }


    /**
     * 处理从服务器上返回的accessToken的结果
     * @param accessTokenResp 调用accessToken服务返回对象
     * @returns 
     */
    private handleAccessToken(accessTokenResp:SoaResponseInfo):Optional<string>{
         let optionAccessToken=accessTokenResp.getFirstTableColumnValue('access_token')
         if(optionAccessToken.isPresent()){//调用服务成功
             this.accessToken=optionAccessToken.get()
             let currDate=new Date()
             let optionExpireTime=accessTokenResp.getFirstTableColumnValue('expire_time')
             this.accessTokenExpireTime=new Date(currDate.getTime()+(Number(optionExpireTime.get())*1000))
             return optionAccessToken
         }
         return optionAccessToken

    }

    /**
     * 获取accessToken,本地有的直接取本地
     * @param companyId 公司id
     * @param staffId 员工id
     * @returns 
     */
    public getAccessToken(companyId:string,staffId:string):Observable<string>{
        if(this.accessTokenExpireTime && this.accessTokenExpireTime>new Date()){
            return of(this.accessToken)
        } 

        this.log.info('accessToken is null so call GetAccessToken from server')
        return this.callService(companyId,staffId,ACCESSTOKEN_SERVICE,
            null,null,5000,this.currStaffUrl).pipe(
            switchMap(axiosResp => 
            {
                let optionSoaResp=this.convertAxiosResponseToSoaResponse(axiosResp,ACCESSTOKEN_SERVICE)
                if(optionSoaResp.isPresent()){
                    let optionAcessToken=this.handleAccessToken(optionSoaResp.get())
                    if(optionAcessToken.isPresent()){
                        return of(optionAcessToken.get())
                    }
                }
                //调用 accessToken服务错误，走备用url地址
                let backupUrlCall=this.switchBackupUrl(companyId,staffId,ACCESSTOKEN_SERVICE,
                    null,axiosResp)
                let backupAccessToken$=backupUrlCall.pipe(
                    map(soaResp=>{
                        let optionAccessToken=this.handleAccessToken(soaResp)
                        if(optionAccessToken.isPresent()){//调用服务成功
                            return optionAccessToken.get()
                        }
                        throw new Error('call GetAccessToken error reason:'+soaResp.getResultMessage())

                    })
                )
                return backupAccessToken$
            })
        )

    }

    /**
     * 调用 服务,如果服务调用失败，有做异常保护，需要判断response.status===200
     * @param companyId 公司ID,null表示spdn服务
     * @param staffId 员工ID,null表示spdn服务
     * @param serviceName 服务名
     * @param accessToken acessToken
     * @param inputBody 输入参数
     * @param timeout ms的超时
     * @param url 调用的url地址
     * @returns 服务的observable
     */
    private callService(companyId:string,staffId:string,
        serviceName:string,accessToken:string,
        inputBody:object,
        timeout:number=DEFAULT_TIMEOUT,
        url:string):Observable<AxiosResponse<any, any>>{
        const timeStamp=this.getTimeStamp()
        let inputParam=null
        let postUrl=null
         if(serviceName==='GetAccessToken'){
             inputParam={
                 token_type:'staff',
                 timestamp:timeStamp,
                 authenticator:Crypt.hex_md5(companyId+staffId+timeStamp),
                 product:'c3AgentPlus'
             }
             postUrl=url+staffId+'/'+serviceName

        //spdn服务，输入参数
        }else if(companyId==null && staffId==null && accessToken==null){
             inputParam={
                 head:{
                     timestamp:timeStamp,
                     authenticator:Crypt.hex_md5(timeStamp)
                 },
                 content:inputBody
             }
             postUrl=url+serviceName
        // staff类型输入参数
        } else  {
            inputParam={
                head:{
                   access_token:accessToken,
                   timestamp:timeStamp,
                   authenticator:Crypt.hex_md5(companyId+staffId+timeStamp),
                   product:'c3AgentPlus'
                },
                content:inputBody
            }
            postUrl=url+staffId+'/'+serviceName
        }
        const inputStr=JSON.stringify(inputParam)
        this.log.debug('call service:'+serviceName+' url:'+url+', request params:'+inputStr)
        let promise=axios.post(postUrl,inputStr,{
            timeout:timeout,
            headers:{
                'Content-Type': 'application/json;charset=utf-8'
            }
        })
        return from(promise)
               .pipe(
                   //写正常的返回结果日志
                 tap(response =>{
                   this.log.debug('call service:'+serviceName+' result:'+JSON.stringify(response.data))
                 }),
                 //如果调用服务失败,这里捕捉后，在上层进行处理
                catchError(axiosError=>{
                    let errorObject:any=null
                    if(axiosError.message && axiosError.message.startsWith('timeout of')){
                       errorObject={
                           status:AXIOS_STATUS_TIMEOUT,
                           statusText:axiosError.message
                       }
                    } else {
                       errorObject={
                           status:120,
                           statusText:axiosError.message
                       }
                    }
                    this.log.warn('call service,url:'+url+' error:'+axiosError.message)
                    return of(errorObject)
                }
                    ) )
    }
    public getCurrSpdnUrl():string{
        return this.currSpdnUrl
    }

    /**
     *  axiosResp url地址返回正确情况下,信息填充到responseInfo中，如果无法填充返回false
     * @param axiosResp axios调用返回的结果
     * @param soaResponse 需要解析的response对象
     * @returns 填充responseInfo是否成功
     */
    private convertAxiosResponseToSoaResponse(axiosResp:AxiosResponse<any, any>,serviceName:string):Optional<SoaResponseInfo>{
         let soaResponse:SoaResponseInfo=null
        if(axiosResp.status===200){
          soaResponse=new SoaResponseInfo(serviceName)
          if(axiosResp.data){
              soaResponse.initResponse(axiosResp.data)
          } else {
              soaResponse.setResultCode(-33)
              soaResponse.setResultMessage("axios response don't have data attribute")
              this.log.error("axios response don't have data attribute,resp:"+axiosResp.toString())
              console.error("axios response don't have data attribute,resp:",axiosResp)
          }
        } 
        return Optional.ofNullable(soaResponse)

    }

    /**
     * 调用spdn服务,返回 observable的结果集
     * @param serviceName spdn类型的服务名
     * @param inputBody 输入参数
     * @returns observable的服务调用结果
     */
    public callCommonService(serviceName:string,inputBody:object):Observable<SoaResponseInfo> {

        if(!serviceName ||serviceName===''){
            let errResponse=new SoaResponseInfo('unknowService')
            errResponse.setResultCode(-19)
            errResponse.setResultMessage('serviceName is empty')
            return of(errResponse)
        } else if( !inputBody){
            let errResponse=new SoaResponseInfo(serviceName)
            errResponse.setResultCode(-19)
            errResponse.setResultMessage('inputParam is empty')
            return of(errResponse)
        }
       let spdn$=this.getSPDNService(serviceName,inputBody)
       const commonService$=spdn$.pipe(
           switchMap(axiosResp => {
               //调用服务正常
               const optionSoap=this.convertAxiosResponseToSoaResponse(axiosResp,serviceName)
               if(optionSoap.isPresent()){
                   return of(optionSoap.get())
               }
               //调用服务出现异常，走备用IP接口
               return this.switchBackupUrl(null,null,serviceName,inputBody,axiosResp)
           })
       )
       return commonService$
    }

    /**
     * 调用c3iservuc的spdn服务返回abservable
     * @param serviceName 服务名
     * @param inputBody 服务输入的object
     * @param timeout 超时时间,ms单位
     * @param url 调用的url地址
     * @returns  Observable
     */
    private getSPDNService(serviceName:string,inputBody:object,
        timeout:number=DEFAULT_TIMEOUT,url=this.currSpdnUrl):Observable<AxiosResponse<any, any>>{
            return this.callService(null,null,serviceName,null,inputBody,timeout,url)
    }
    /**
     * 返回当前的yyyymmddhh24miss格式的当前时间
     * @returns yyyymmddhh24miss格式
     */
    private getTimeStamp():string{
        let date=new Date( );
        let year = date.getFullYear() // 年
        let month = date.getMonth() + 1; // 月
        let day  = date.getDate(); // 日
        let hour = date.getHours(); // 时
        let minutes = date.getMinutes(); // 分
        let seconds = date.getSeconds() //秒

        let timeStr=year.toString()
                   +((month<=9)?("0"+month.toString()):month.toString())
                   +((day<=9)?("0"+day.toString()):day.toString())
                   +((hour<=9)?("0"+hour.toString()):hour.toString())
                   +((minutes<=9)?("0"+minutes.toString()):minutes.toString())
                   +((seconds<=9)?("0"+seconds.toString()):seconds.toString())
        return timeStr
    }

    
    /**
     * 采用备用的url地址调用,如果调用成功把备用地址切成正式地址
     * @param companyId staff服务的公司ID，如果是spdn服务可空
     * @param staffId staff服务的员工ID，如果是spdn服务可空
     * @param serviceName 服务名
     * @param inputBody 输入参数
     * @param axiosError 调用axios服务失败返回的对象
     * @returns 调用备用SPDN服务的observable列表
     */
    private switchBackupUrl(companyId:string,staffId:string,
        serviceName:string,inputBody:object,
        axiosError:AxiosResponse<any, any>):Observable<SoaResponseInfo>{
        this.log.warn('call service('+serviceName+') error,  use backup url call service')
        const isStaffService=(staffId==null&&companyId==null)?false:true
        const backupUrls=(isStaffService)?this.staffUrls:this.spdnUrls
        //调用服务错误,触发是异常处理
        // 1.如果是调用服务超时,就不进行地址切换，返回服务失败
        // 2.如果是500,404之类的错误，表示应用服务器异常,需要进行url地址切换
        // 3.如果没有备用的url地址,直接返回失败
        let soaResponseInfo=new SoaResponseInfo(serviceName)
        if(axiosError.status===AXIOS_STATUS_TIMEOUT){
            this.log.error(' because call service is timeout,so return error ')
            soaResponseInfo.setResultCode(axiosError.status)
            soaResponseInfo.setResultMessage(axiosError.statusText)
            return  of(soaResponseInfo)
        }
        if(backupUrls.length<=1){
            this.log.error("because not have backup url to switch,so error")
            soaResponseInfo.setResultCode(-34)
            soaResponseInfo.setResultMessage('call service error,status:'+axiosError.status+',And not backup url to switch')
            return  of(soaResponseInfo)
        }
        /**
         * 1.把备用的urls地址生成obserable源,
         * 2.根据备用的urls地址映射成axios的obserable，
         *  concatMap操作符:按顺序调用axios的C3_GetEccParams
         * 3.tableWhile操作符: 如果调用失败就继续调用(true),如果服务调用成功就停止调用(false)
         */
        let backupSPDNService$:Observable<SoaResponseInfo>=from(backupUrls.slice(1)).pipe(
            // 把urls地址转换成按顺序 调用 spdnService的Observer源
            concatMap(backupUrl =>{
               this.log.info('use backupUrl:'+backupUrl+' to call service ')
               if(isStaffService){
                   this.currStaffUrl=backupUrl
               } else {
                   this.currSpdnUrl=backupUrl
               }
               //第一个url就是currSpdnUrl,但currSpdnUrl调用异常了。把第一个URL地址取出来，放到末尾
               backupUrls.push(backupUrls.shift())
               if(isStaffService){
                   return this.callService(companyId,staffId,serviceName,this.accessToken,inputBody,5000,backupUrl)
               } else {
                  return this.getSPDNService(serviceName,inputBody,5000,backupUrl)
               }
            }),
            // 如果服务调用成功，剩下的spdnService的Observer就不要调用了
            first(backupAxiosResp=>{
                if(backupAxiosResp.status ==200 || backupAxiosResp.status==AXIOS_STATUS_TIMEOUT){
                    //false 取消后面的obserable,true:继续跑
                    return true
                } else {
                    return false
                }
            },null),
            //把axiosResponse转换为SoaReponseInfo的Observer
            map(axiosResp=>{
              let soaResponse:SoaResponseInfo=new SoaResponseInfo(serviceName)
              //说明，接口一直调用不成功,给个默认值
              if(axiosResp==null){
                  soaResponse.setResultCode(-32)
                  soaResponse.setResultMessage('all backup url call service error,please concat administrator')
                  return soaResponse
              }
              if(axiosResp.status!=200){
                  this.log.error(' because call service is timeout,so return error,status:'+axiosResp.status)
                  soaResponse.setResultCode(axiosError.status??-30)
                  soaResponse.setResultMessage(axiosError.statusText??"unkown error message")
                  return  soaResponse
              }
              const optionSoaResp= this.convertAxiosResponseToSoaResponse(axiosResp,serviceName)
              if(optionSoaResp.isPresent()){
                  return optionSoaResp.get()
              }
              throw new Error('impossible throw this message,aiosRep:'+axiosResp.toString())
              //return throwError(()=>new Error('impossible throw this message,aiosRep:'+axiosResp.toString()))
            })
        )
        return backupSPDNService$
    }
    
}

